import datetime
from typing import List, Optional, Tuple

import coin.proto.options_pricing_pb2 as op_pb2
import coin.support.options_pricing.sabr as sabr
from coin.base.datetime_util import to_datetime, to_timestamp_int
from coin.exchange.base.kr_rest.py_product import PyProduct
from coin.exchange.kr_rest.product.product_impl import generate_product_from_str2
from coin.proto.coin_market_query_pb2 import ExchangeTicker, ProductTicker
from coin.support.options_pricing.types import OptionsGreeks


def _calculate_interpolated_price(base: str,
                                  quote: str,
                                  expiry_dt: datetime.datetime,
                                  tickers: List[ProductTicker],
                                  curr_dt: Optional[datetime.datetime]
) -> Optional[float]:
  ret = None
  candidates: List[Tuple[datetime.datetime, float]] = []
  for ticker in tickers:
    try:
      product = generate_product_from_str2(
          ticker.market_type, ticker.exchange, None, ticker.symbol, curr_dt)
    except:
      continue
    if product.market_name == 'Futures' and \
       product.base.currency == base and \
       product.quote.currency == quote:
      if product.expiry_str is None or product.expiry_str == 'PERPETUAL':
        continue
      candidate = (datetime.datetime.strptime(product.expiry_str, '%Y%m%d'), ticker.last)
      candidates.append(candidate)
  candidates = sorted(candidates, key=lambda x: x[0])
  for first, second in zip(candidates, candidates[1:]):
    first_dt, first_price = first
    second_dt, second_price = second
    if first_dt <= expiry_dt <= second_dt:
      if first_dt == second_dt:
        ret = first_price
      else:
        delta1 = expiry_dt - first_dt
        delta2 = second_dt - first_dt
        delta_price = second_price - first_price
        ret = first_price + delta1.total_seconds() / delta2.total_seconds() * delta_price
      break
  return ret


def calculate_sabr_greeks(product: PyProduct,
                          exchange_tickers: List[ExchangeTicker],
                          sabr_params: List[op_pb2.SabrPricingParamsProto],
                          current_datetime: Optional[datetime.datetime],
) -> OptionsGreeks:
  greeks = OptionsGreeks()
  pricing_param = None
  for param in sabr_params:
    expiry_date = to_datetime(param.expiry_time)
    if param.exchange == product.exchange_name and \
       param.base == product.base.currency and \
       param.quote == product.quote.currency and \
       expiry_date.strftime('%Y%m%d') == product.expiry_str:
      pricing_param = param
      break
  if pricing_param is not None:
    candidate = None
    for exchange_ticker in exchange_tickers:
      if exchange_ticker.exchange == pricing_param.exchange and \
         exchange_ticker.market_type == 'Futures':
        candidate = exchange_ticker
        break
    if candidate is not None:
      expiry_dt = to_datetime(pricing_param.expiry_time)
      symbol = '%s-%s.%s' % (
          pricing_param.base,
          pricing_param.quote,
          expiry_dt.strftime('%Y%m%d'))
      func = lambda symbol, ticker: [
        each_ticker.last for each_ticker in ticker.each_ticker
        if each_ticker.symbol == symbol]
      prices = func(symbol, candidate)
      price = prices[0] if len(prices) > 0 else None
      if price is None:
        for elem in candidate.each_ticker:
          elem.market_type = candidate.market_type
          elem.exchange = candidate.exchange
        price = _calculate_interpolated_price(
            pricing_param.base, pricing_param.quote, expiry_dt,
            candidate.each_ticker, current_datetime)
      if price is not None:
        implied_volatility = \
            sabr.calculate_implied_volatility(F=price,
                                              K=product.strike_price,
                                              T=pricing_param.expiry_time,
                                              alpha=pricing_param.alpha,
                                              beta=pricing_param.beta,
                                              rho=pricing_param.rho,
                                              nu=pricing_param.nu,
                                              curr_ts=to_timestamp_int(current_datetime))
        delta = sabr.calculate_delta(F=price,
                                     K=product.strike_price,
                                     T=pricing_param.expiry_time,
                                     r=0.01,
                                     side=product.option_side,
                                     alpha=pricing_param.alpha,
                                     beta=pricing_param.beta,
                                     rho=pricing_param.rho,
                                     nu=pricing_param.nu,
                                     curr_ts=to_timestamp_int(current_datetime))
        gamma = sabr.calculate_gamma(F=price,
                                     K=product.strike_price,
                                     T=pricing_param.expiry_time,
                                     r=0.01,
                                     side=product.option_side,
                                     alpha=pricing_param.alpha,
                                     beta=pricing_param.beta,
                                     rho=pricing_param.rho,
                                     nu=pricing_param.nu,
                                     curr_ts=to_timestamp_int(current_datetime))

        vega = sabr.calculate_vega(F=price,
                                   K=product.strike_price,
                                   T=pricing_param.expiry_time,
                                   r=0.01,
                                   side=product.option_side,
                                   alpha=pricing_param.alpha,
                                   beta=pricing_param.beta,
                                   rho=pricing_param.rho,
                                   nu=pricing_param.nu,
                                   curr_ts=to_timestamp_int(current_datetime))
        theta = sabr.calculate_theta(F=price,
                                     K=product.strike_price,
                                     T=pricing_param.expiry_time,
                                     r=0.01,
                                     side=product.option_side,
                                     alpha=pricing_param.alpha,
                                     beta=pricing_param.beta,
                                     rho=pricing_param.rho,
                                     nu=pricing_param.nu,
                                     curr_ts=to_timestamp_int(current_datetime))
        gamma_in_quote = None
        if gamma is not None:
          gamma_in_quote = gamma * price ** 2 / 100
        greeks = OptionsGreeks(
            implied_volatility=implied_volatility,
            delta=delta, gamma=gamma, vega=vega, theta=theta,
            gamma_in_quote=gamma_in_quote, vega_in_quote=vega, theta_in_quote=theta)
  return greeks
